package com.tosmart.jorm.impl;

import com.tosmart.jorm.face.ColumnSelector;
import com.tosmart.jorm.face.OptimisticLockSupport;
import com.tosmart.jorm.util.DBAssistant;

import java.lang.reflect.Field;
import java.sql.Connection;
import java.util.*;

/**
 * JormFace's implements ...
 * <p/>
 * Description of the various ways in which
 * information, please parameter JormFace class.
 *
 * @author tosmart@gmail.com
 * @see com.tosmart.jorm.face.JormFace
 */
public class JormImpl {

    public JormImpl(Connection connection) {
        this.connection = connection;
    }

    public <T> T createEntity(
            Class<T> entityClass, Map<String, Object> map) {

        T entity;
        try {
            entity = entityClass.newInstance();
        }
        catch (Exception e) {
            System.err.println("Entity mush have *default-constructor*!");
            throw new RuntimeException(e);
        }

        try {
            JormReflect.setFieldValues(entity, map);
        }
        catch (IllegalAccessException e) {
            throw new RuntimeException(e);
        }

        loadReferAndReferList(entity);

        if (JormReflect.useOptimisticLock(entity.getClass())) {
            setLockVersion(entity, map);
        }

        return entity;
    }

    public <T> T loadEntity(
            Class<T> entityClass, Object entityId) {

        String sql = SqlBuilder.generateLoadSql(entityClass);
        Object[] values = JormReflect.idValues(entityId);

        DBAssistant dba = new DBAssistant(connection, false);
        List<Map<String, Object>> maps = dba.select(sql, values);

        if (maps.isEmpty()) return null;

        return createEntity(entityClass, maps.get(0));
    }

    public int insertEntity(Object entity) {

        String sql = SqlBuilder.generateInsertSql(
                entity.getClass()
        );

        String[] columns = sql.substring(
                sql.indexOf('(') + 1,
                sql.indexOf(')')
        ).split(",");

        Object[] values;
        try {
            values = getColumnValues(entity, columns);
        }
        catch (IllegalAccessException e) {
            throw new RuntimeException(e);
        }

        DBAssistant dba = new DBAssistant(connection, false);
        return dba.execute(sql, values);
    }

    public int deleteEntity(Object entity) {

        String sql = SqlBuilder.generateDeleteSql(
                entity.getClass()
        );
        Object[] values = JormReflect.idValues(entity);

        DBAssistant dba = new DBAssistant(connection, false);
        return dba.execute(sql, values);
    }

    public int updateEntity(Object entity) {

        String sql = SqlBuilder.generateUpdateSql(
                entity.getClass()
        );

        List<Field> fields = JormReflect.
                getMapFields(entity.getClass());

        List<Object> values;
        try {
            values = JormReflect.getFieldValuesExceptPk(
                    entity,
                    fields
            );
        }
        catch (IllegalAccessException e) {
            throw new RuntimeException(e);
        }

        List<Object> pkValues;
        try {
            pkValues = JormReflect.getPkFieldValues(
                    entity,
                    fields
            );
        }
        catch (IllegalAccessException e) {
            throw new RuntimeException(e);
        }
        values.addAll(pkValues);

        boolean useOptimisticLock = JormReflect.
                useOptimisticLock(entity.getClass());

        if (useOptimisticLock) {
            OptimisticLockSupport impl =
                    (OptimisticLockSupport) entity;
            values.add(impl.getVersion());
        }

        DBAssistant dba = new DBAssistant(connection, false);
        int rows = dba.execute(sql, values.toArray());

        if (useOptimisticLock && rows < 1) {
            checkLockException(entity, pkValues);
        }

        return rows;
    }

    public <T> List<T> listEntities(Class<T> entityClass) {

        String sql = SqlBuilder.generateListSql(entityClass);

        DBAssistant dba = new DBAssistant(connection, false);
        List<Map<String, Object>> maps = dba.select(sql);

        List<T> list = new ArrayList<T>();
        for (Map<String, Object> map : maps) {
            list.add(createEntity(entityClass, map));
        }

        return list;
    }

    public <T> List<T> find(Class<T> entityClass,
                            String[] columns, Object[] values) {

        if (columns == null || columns.length == 0)
            return listEntities(entityClass);

        if (columns.length != values.length) {
            throw new RuntimeException(
                    "Argument column count not same value count.");
        }

        StringBuilder sql = new StringBuilder();
        sql.append(SqlBuilder.generateListSql(entityClass));
        sql.append(' ').append(
                SqlBuilder.generateWhereClause(columns)
        );

        DBAssistant dba = new DBAssistant(connection, false);
        List<Map<String, Object>> maps =
                dba.select(sql.toString(), values);

        List<T> list = new ArrayList<T>();
        for (Map<String, Object> map : maps) {
            list.add(createEntity(entityClass, map));
        }

        return list;
    }

    public <T> List<T> findEx(
            Class<T> entityClass, String addClause,
            Object... params) {

        StringBuilder sql = new StringBuilder();
        sql.append(SqlBuilder.generateListSql(entityClass));
        sql.append(' ').append(addClause);

        DBAssistant dba = new DBAssistant(connection, false);
        List<Map<String, Object>> maps =
                dba.select(sql.toString(), params);

        List<T> list = new ArrayList<T>();
        for (Map<String, Object> map : maps) {
            list.add(createEntity(entityClass, map));
        }

        return list;
    }

    public <T> List<T> exampleFind(
            T exampleEntity, ColumnSelector selector,
            boolean enableStringLike,
            String addClause, Object... params) {

        List<Object> values = new ArrayList<Object>();

        String findSql;
        try {
            findSql = composeColumns(
                    exampleEntity,
                    selector,
                    enableStringLike,
                    values
            );
        }
        catch (IllegalAccessException e) {
            throw new RuntimeException(e);
        }

        String sql = findSql + ' ' + addClause;
        values.addAll(Arrays.asList(params));

        DBAssistant dba = new DBAssistant(connection, false);
        List<Map<String, Object>> maps =
                dba.select(sql, values.toArray());

        @SuppressWarnings({"unchecked"})
        Class<T> entityClass =
                (Class<T>) exampleEntity.getClass();
        List<T> entities = new ArrayList<T>();

        for (Map<String, Object> map : maps) {
            entities.add(createEntity(entityClass, map));
        }

        return entities;
    }

    private static <T> void setLockVersion(
            T entity, Map<String, Object> map) {

        String versionColumn = JormReflect.optimisticLock(
                entity.getClass()
        );

        Object versionObject = map.get(versionColumn.toUpperCase());
        Long version = SqlTypeAdapter.toJavaLong(versionObject);

        ((OptimisticLockSupport) entity).setVersion(version);
    }

    private Object[] getColumnValues(
            Object entity, String[] columns)
            throws IllegalAccessException {

        Object[] values = new Object[columns.length];

        for (int i = 0; i < columns.length; i++) {

            Field field = JormReflect.findField(
                    entity.getClass(),
                    columns[i].trim()
            );

            field.setAccessible(true);
            values[i] = field.get(entity);

            if (NumberAutoGenerate.need(values[i], field)) {
                values[i] = new NumberAutoGenerate(connection)
                        .generateValue(entity, field);
            }
        }
        return values;
    }

    private String composeColumns(
            Object entity, ColumnSelector selector,
            boolean enableLikeString, List<Object> values)
            throws IllegalAccessException {

        List<Field> fields = JormReflect.
                getMapFields(entity.getClass());

        StringBuilder whereClause = new StringBuilder();

        for (Field field : fields) {

            field.setAccessible(true);
            Object value = field.get(entity);

            if (selector.include(field.getName(), value)) {

                String column = JormReflect.getOrmMapColumn(field);
                whereClause.append(column);

                if (enableLikeString &&
                        String.class.equals(field.getType())) {
                    whereClause.append(" LIKE ? AND ");
                    values.add("%" + value + "%");
                }
                else {
                    whereClause.append("=? AND ");
                    values.add(value);
                }
            }
        }

        if (whereClause.length() > 0) {
            whereClause.delete(
                    whereClause.lastIndexOf(" AND "),
                    whereClause.length()
            );
        }

        String sql = SqlBuilder.generateListSql(
                entity.getClass()
        );

        if (whereClause.length() > 0)
            sql += " WHERE " + whereClause;

        return sql;
    }

    private <T> void loadReferAndReferList(T entity) {

        // Related to the stack overflow to prevent
        // the alternating
        if (!ReferStack.get().forward()) return;

        try {
            doLoadRefer(entity);
            doLoadReferList(entity);
        }
        catch (IllegalAccessException e) {
            e.printStackTrace();
        }
        finally {
            // Related to the stack overflow to prevent
            // the alternating
            ReferStack.get().back();
        }
    }

    private void doLoadRefer(Object entity)
            throws IllegalAccessException {

        List<Field> referFields =
                JormReflect.getReferFields(entity.getClass());

        for (Field referField : referFields) {

            String[] foreignKeys =
                    JormReflect.getOrmReferForeignKeys(referField);

            if (foreignKeys.length == 0) {
                throw new RuntimeException(
                        "Not set 'foreignKeys' argument for @JormRefer");
            }

            List<Field> primaryKeyFields =
                    JormReflect.getPkFields(referField.getType());

            StringBuilder addClause = new StringBuilder(" WHERE ");
            for (Field field : primaryKeyFields) {
                String column = JormReflect.getOrmMapColumn(field);
                addClause.append(column).append("=? AND ");
            }
            addClause.delete(
                    addClause.lastIndexOf(" AND "),
                    addClause.length()
            );

            List entities = findEx(
                    referField.getType(),
                    addClause.toString(),
                    JormReflect.getColumnValues(entity, foreignKeys)
            );

            referField.setAccessible(true);
            referField.set(entity,
                    entities.isEmpty() ? null : entities.get(0));
        }
    }

    private void doLoadReferList(Object entity)
            throws IllegalAccessException {

        List<Field> referListFields = JormReflect.
                getReferListFields(entity.getClass());

        for (Field referListField : referListFields) {

            Class referEntityClass = JormReflect.
                    getOrmReferListEntityClass(referListField);

            if (referEntityClass == null) {
                throw new RuntimeException(
                        "No 'entityClass' argument for @JormReferList");
            }

            String[] foreignKeys = JormReflect.
                    getOrmReferListForeignKeys(referListField);

            List<Field> primaryKeyFields =
                    JormReflect.getPkFields(entity.getClass());

            if (foreignKeys.length == 0 ||
                    foreignKeys.length != primaryKeyFields.size()) {

                throw new RuntimeException(
                        "Error 'foreignKeys' argument for @JormReferList");
            }

            @SuppressWarnings({"unchecked"})
            List entities = findEx(
                    referEntityClass,
                    SqlBuilder.generateWhereClause(foreignKeys),
                    JormReflect.getFieldValues(entity, primaryKeyFields)
            );

            referListField.setAccessible(true);
            referListField.set(entity, entities);
        }
    }

    private void checkLockException(
            Object entity, List<Object> pkValues) {

        OptimisticLockSupport impl =
                (OptimisticLockSupport) entity;
        Long version = impl.getVersion();

        String sql = SqlBuilder.generateReadLockVersionSql(
                entity.getClass()
        );

        DBAssistant dba = new DBAssistant(connection, false);
        Long dbVersion = dba.selectLong(
                sql,
                pkValues.toArray()
        );

        if (dbVersion > version) {
            throw new RuntimeException(
                    "Update Fail, Possible Lock Object is Changed!"
            );
        }
    }

    private Connection connection;
}
